using System;
using System.Diagnostics.Contracts;
using System.IO;

namespace OpenCube.IO
{
	[CLSCompliant(false)]
	public sealed class EndiannessAwareBinaryReader
		: IBinaryReader
	{
		private readonly Stream _stream;
		private readonly byte[] _buffer;

		private readonly bool _reverse = false;

		/// <summary>
		/// Create a new instance of a <see cref="EndiannessAwareBinaryReader"/> that reads from the given <see cref="Stream"/>.
		/// </summary>
		/// <param name="stream">The stream to read from.</param>
		/// <param name="streamEndianness">The endianess of the stream to read from.</param>
		public EndiannessAwareBinaryReader(Stream stream, Endianness streamEndianness)
		{
			Contract.Requires<ArgumentNullException>(stream != null, "A stream is required");
			Contract.Requires<ArgumentOutOfRangeException>(Enum.IsDefined(typeof(Endianness), streamEndianness), "The given endianness is not understood or supported.");
			Contract.Requires<ArgumentOutOfRangeException>(streamEndianness != Endianness.None, "An endianness must be given");
			Contract.Requires<InvalidOperationException>(stream.CanRead, "The stream should be readable");

			Contract.Ensures(_stream != null);
			Contract.Ensures(_stream == stream);
			Contract.Ensures(_buffer != null);
			Contract.Ensures(_buffer.Length == 16);

			// determine endianness of this system
			Endianness systemEndianness = BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian;
			// reverse if system and stream endianess do not match.
			_reverse = systemEndianness != streamEndianness;

			_buffer = new byte[16];
			_stream = stream;
		}

		[ContractInvariantMethod]
		private void ContractInvariant()
		{
			Contract.Invariant(_stream != null);
			Contract.Invariant(_buffer != null);
			Contract.Invariant(_buffer.Length == 16);
		}

		/// <summary>
		/// Read a single byte from the stream.
		/// </summary>
		/// <returns>The byte read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there is less as 1 byte available on the stream</exception>
		public byte ReadByte()
		{
			int readBytes = _stream.Read(_buffer, 0, 1);
			if (readBytes == 0)
				throw new EndOfStreamException("There where no bytes available on the stream.");

			return _buffer[0];
		}

		/// <summary>
		/// Read a single signed byte from the stream.
		/// </summary>
		/// <returns>The signed byte read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there is less as 1 byte available on the stream</exception>
		public sbyte ReadSByte()
		{
			int readBytes = _stream.Read(_buffer, 0, 1);
			if (readBytes == 0)
				throw new EndOfStreamException("There where no bytes available on the stream.");

			return (sbyte)_buffer[0];
		}

		/// <summary>
		/// Read a signed 16 bit integer from the stream.
		/// </summary>
		/// <returns>The signed 16 bit integer read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 2 bytes available on the stream</exception>
		public short ReadInt16()
		{
			int read = _stream.Read(_buffer, 0, 2);
			if (read != 2)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a (signed) Int16 requires there to be 2.");

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 0, 2);

			return BitConverter.ToInt16(_buffer, 0);
		}

		/// <summary>
		/// Read a unsigned 16 bit integer from the stream.
		/// </summary>
		/// <returns>The unsigned 16 bit integer read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 2 bytes available on the stream</exception>
		public ushort ReadUInt16()
		{
			int read = _stream.Read(_buffer, 0, 2);
			if (read != 2)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a (signed) Int16 requires there to be 2.");

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 0, 2);

			return BitConverter.ToUInt16(_buffer, 0);
		}

		/// <summary>
		/// Read a signed 24 bit integer from the stream.
		/// </summary>
		/// <returns>The signed 24 bit integer read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 3 bytes available on the stream</exception>
		public int ReadInt24()
		{
			_buffer[0] = 0x00;
			_buffer[4] = 0x00;

			int read = _stream.Read(_buffer, 1, 3);
			if (read != 3)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a (signed) Int24 requires there to be 3.");

			// if MSB is set, extend with 0xff to keep correct sign
			if ((_buffer[1] & 0x80) != 0)
			{
				_buffer[0] = 0xff;
				_buffer[4] = 0xff;
			}

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 1, 3);

			return BitConverter.ToInt32(_buffer, 1);
		}

		/// <summary>
		/// Read a unsigned 24 bit integer from the stream.
		/// </summary>
		/// <returns>The unsigned 24 bit integer read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 3 bytes available on the stream</exception>
		public uint ReadUInt24()
		{
			_buffer[0] = 0x00;
			_buffer[4] = 0x00;

			int read = _stream.Read(_buffer, 1, 3);
			if (read != 3)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a (signed) Int24 requires there to be 3.");

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 1, 3);

			return BitConverter.ToUInt32(_buffer, 1);
		}

		/// <summary>
		/// Read a signed 32 bit integer from the stream.
		/// </summary>
		/// <returns>The signed 32 bit integer read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 4 bytes available on the stream</exception>
		public int ReadInt32()
		{
			int read = _stream.Read(_buffer, 0, 4);
			if (read != 4)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a (signed) Int32 requires there to be 4.");

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 0, 4);

			return BitConverter.ToInt32(_buffer, 0);
		}

		/// <summary>
		/// Read a unsigned 32 bit integer from the stream.
		/// </summary>
		/// <returns>The unsigned 32 bit integer read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 4 bytes available on the stream</exception>
		public uint ReadUInt32()
		{
			int read = _stream.Read(_buffer, 0, 4);
			if (read != 4)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a (signed) Int32 requires there to be 4.");


			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 0, 4);

			return BitConverter.ToUInt32(_buffer, 0);
		}

		/// <summary>
		/// Read a signed 64 bit integer from the stream.
		/// </summary>
		/// <returns>The signed 64 bit integer read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 8 bytes available on the stream</exception>
		public long ReadInt64()
		{
			int read = _stream.Read(_buffer, 0, 8);
			if (read != 8)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a (signed) Int64 requires there to be 8.");

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 0, 8);

			return BitConverter.ToInt64(_buffer, 0);
		}

		/// <summary>
		/// Read a unsigned 64 bit integer from the stream.
		/// </summary>
		/// <returns>The unsigned 64 bit integer read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 8 bytes available on the stream</exception>
		public ulong ReadUInt64()
		{
			int read = _stream.Read(_buffer, 0, 8);
			if (read != 8)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a (signed) Int64 requires there to be 8.");

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 0, 8);

			return BitConverter.ToUInt64(_buffer, 0);
		}

		/// <summary>
		/// Read a single precision floating point number from the stream.
		/// </summary>
		/// <returns>The single precision floating point number read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 4 bytes available on the stream</exception>
		public float ReadSingle()
		{
			int read = _stream.Read(_buffer, 0, 4);
			if (read != 4)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a single requires there to be 4.");

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 0, 4);

			return BitConverter.ToSingle(_buffer, 0);
		}

		/// <summary>
		/// Read a double precision floating point number from the stream.
		/// </summary>
		/// <returns>The double precision floating point number read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as 8 bytes available on the stream</exception>
		public double ReadDouble()
		{
			int read = _stream.Read(_buffer, 0, 8);
			if (read != 8)
				throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while a double requires there to be 8.");

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if (_reverse)
				Array.Reverse(_buffer, 0, 8);

			return BitConverter.ToDouble(_buffer, 0);
		}

		/// <summary>
		/// Read the given number of bytes from the stream
		/// </summary>
		/// <param name="count">The number of bytes to read from the stream.</param>
		/// <returns>An array with the number of bytes as read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as the given number of bytes available on the stream</exception>
		public byte[] ReadBytes(int count)
		{
			var buffer = new byte[count];

			if (count > 0)
			{
				int read = _stream.Read(buffer, 0, count);
				if (read != count)
					throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while there where " + count + " requested.");
			}

			return buffer;
		}

		/// <summary>
		/// Read the given number of signed bytes from the stream
		/// </summary>
		/// <param name="count">The number of signed bytes to read from the stream.</param>
		/// <returns>An array with the number of signed bytes as read from the stream.</returns>
		/// <exception cref="EndOfStreamException">Thrown when there are less as the given number of bytes available on the stream</exception>
		[CLSCompliant(false)]
		public sbyte[] ReadSBytes(int count)
		{
			var buffer = new sbyte[count];
			var tempBuffer = new byte[count];

			if (count > 0)
			{
				int read = _stream.Read(tempBuffer, 0, count);
				if (read != count)
					throw new EndOfStreamException("There where only " + read + " bytes available on the stream, while there where " + count + " requested.");

				// copy over, so bytes become signed bytes
				Buffer.BlockCopy(tempBuffer, 0, buffer, 0, read);
			}

			return buffer;
		}
	}
}